#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include "cnet.h"
#include "network.h"

#define LAN_LINK 1
#define IP_CODE 0x0800
#define ETH_CODE 0x0001
#define ARP_CODE 0x0806
#define ARP_REQUEST_CODE 1 
#define ARP_REPLY_CODE 2
#define BROADCAST_ADDR "ff:ff:ff:ff:ff:ff"
#define ARP_PACKET_SIZE 28
#define ETH_FRAME_HEADER_SIZE 14
#define MAX_ADDR 255

/*--------------------------ESTRUCTURAS DE DATOS------------------------------*/
#pragma pack (1)
//paquete ARP
typedef struct
{
	uint16_t htype; // Espacio de direcciones de hardware.
	uint16_t ptype; // Espacio de direcciones del protocolo.
	uint8_t hlen; // Largo, en bytes, de las direcciones de hardware.
	uint8_t plen; // Largo, en bytes, de las direcciones del protocolo.
	uint16_t opcode; // Código de operación: petición o respuesta.
	CnetNICaddr sha; // Dirección de hardware del emisor.
	IPAddress spa; // Dirección de protocolo del emisor.
	CnetNICaddr tha; // Dirección de hardware del receptor del paquete.
	IPAddress tpa; // Dirección de protocolo del receptor.
} ARP_PACKET;

/*tabla ARP; cada host tiene su propia tabla. Las direcciones IP están en el 
  formato de la arquitectura del host */
struct s_arp_table{
	IPAddress ipaddresses[MAX_ADDR];
	CnetNICaddr nicaddresses[MAX_ADDR];
	int	tam;
}arp_table;

//trama ethernet
typedef struct
{
	CnetNICaddr dest; //dirección NIC del destinatario
	CnetNICaddr src; //dirección NIC del emisor
	char type[2]; //tipo de paquete contenido en el payload
	char payload[1000]; //paquete
} ETH_FRAME;

/** Prototipos de las funciones auxiliares **/
void raw_string_to_arp_packet(char *string, ARP_PACKET *arp_pack);
int handle_incoming_arp_packet(ARP_PACKET *arp_pack);
int arp_table_search(const IPAddress ip); 
void update_arp_table(CnetNICaddr na, int pos);
void send_arp_reply(ARP_PACKET *arp_pack);
void add_arp_table(IPAddress ip, CnetNICaddr na);
void print_arp_packet(ARP_PACKET pack);
void print_eth_frame(ETH_FRAME frame);

/*---------------------------MANEJADORES DE EVENTOS---------------------------*/


/* Manejador frente al evento EV_PHYSICAL_READY; i.e, ha llegado una trama a la
   capa física desde otro host. El tipo viene en la arquitectura de la red, en-
   tonces hay que pasarlo a la del host
*/
static EVENT_HANDLER(recibir_trama)
{
	size_t max_size = LAN_MAXPACKET; //tamaño máximo de una trama ethernet
	int link = LAN_LINK; //enlace desde el cual vamos a leer
	char *buffer = NULL; //buffer donde se almacena lo leido de la capa física
	ETH_FRAME frame; //la trama recibida
	char *aux = NULL; //puntero auxiliar para armar la trama a partir del buffer
	size_t payload_size = 0; //tamaño del payload de la trama recibida
	bool msg_for_me = false;
	char sender[18];
	
	buffer = (char *) calloc (max_size, sizeof(char));
	assert (buffer != NULL);
	
	//leemos A LO SUMO max_size_bytes
	CHECK(CNET_read_physical(&link, (void *) buffer, &max_size));
	//ahora max_size contiene la cantidad de bytes realmente leídos
	
	payload_size = max_size - ETH_FRAME_HEADER_SIZE;
	
	if (payload_size < LAN_MINDATA) //paquete demasiado chico
		printf("Paquete demasiado chico. Descartando\n");
	else
	{
		char deststr[18];
		char myNICaddr[18];
		uint16_t code = 0;
		
		//armamos la trama ETHERNET
		aux = buffer;
		memcpy(frame.dest, aux, sizeof(CnetNICaddr));
		aux += sizeof(CnetNICaddr);
		memcpy(frame.src, aux, sizeof(CnetNICaddr));
		aux += sizeof(CnetNICaddr);
		memcpy(frame.type, aux, sizeof(frame.type));
		aux += sizeof(frame.type);
		memcpy(frame.payload, aux, payload_size);
				
		CNET_format_nicaddr(deststr, frame.dest);
		CNET_format_nicaddr(myNICaddr, linkinfo[LAN_LINK].nicaddr);

		msg_for_me = (strncmp(myNICaddr, deststr,18) == 0);
		msg_for_me = msg_for_me || (strncmp(BROADCAST_ADDR,deststr,18)==0);
		CNET_format_nicaddr(sender, frame.src);
		
		memcpy(&code, frame.type, sizeof(frame.type));
		code = ntohs(code);
		
		if (msg_for_me) //el mensaje es para mi
		{
			if (code == ARP_CODE)
			{
				ARP_PACKET *arp_pack = NULL;
				int err = 0;
				arp_pack = (ARP_PACKET *) malloc (sizeof(ARP_PACKET));
				assert (arp_pack != NULL);
			
				raw_string_to_arp_packet(frame.payload, arp_pack);
				err = handle_incoming_arp_packet(arp_pack);
				free(arp_pack);
				arp_pack = NULL;
			}
			else if (code == IP_CODE)
			{		
				printf("Recibido paquete IP de %s\n", sender);
				//paquete IP => se lo pasamos a la capa de red
				network_write_network((void *) frame.payload, &payload_size);
			}
			else
				printf("Tipo de paquete desconocido=0x0%x. Descartando\n", code);
		}
	}
	free(buffer);
	buffer = NULL;
}

/* Manejador frente al evento EV_NETWORK_READY; i.e, la capa de red tiene un pa-
   quete listo para enviar hacia otro host.
*/
static EVENT_HANDLER(enviar_trama)
{
	char *buffer = NULL; //buffer donde se almacena el paquete
	IPAddress destIP = 0; //dirección IP del destinatario
	size_t max_pack_size = LAN_MAXDATA; //maximo tamaño del paquete
	int pos = 0; //indice de búsqueda en la tabla ARP
	ETH_FRAME toSend; //trama a ser enviada
	int link = LAN_LINK; //enlace por donde se va a enviar la trama
	size_t len = 0;
	CnetNICaddr ownNICaddr; //mi dirección NIC
	uint16_t code = 0;
	
	buffer = (char *) calloc (max_pack_size, sizeof(char));
	assert (buffer != NULL);
	
	memcpy(ownNICaddr, linkinfo[LAN_LINK].nicaddr, sizeof(CnetNICaddr));
	
	network_read_network(&destIP, (void *) buffer, &max_pack_size);
	//max_pack_size contiene ahora el número de bytes escritos
	
	//buscamos a ver si tenemos la dirección NIC asociada a la dirección IP
	pos = arp_table_search(destIP);
	if (pos != -1) //tengo la dirección NIC!
	{
		CnetNICaddr destNIC;
		memcpy(destNIC, arp_table.nicaddresses[pos], sizeof(CnetNICaddr));
		code = htons(IP_CODE);
		
		memcpy(toSend.dest, destNIC, sizeof(CnetNICaddr));
		memcpy(toSend.src, ownNICaddr, sizeof(CnetNICaddr));
		memcpy(toSend.type, &code, sizeof(toSend.type));
		memcpy(toSend.payload, buffer, max_pack_size);
		if (max_pack_size < LAN_MINDATA) //paquete muy chico => rellenamos
		{
			int i = 0;
			for (i=max_pack_size-1; i < LAN_MINDATA; i++)
				toSend.payload[i] = '\0';
		}
	}
	else //no la tengo => broadcast pedido ARP
	{
		ARP_PACKET arp_request;
		code = htons(ARP_CODE);

		CNET_parse_nicaddr(toSend.dest, BROADCAST_ADDR);
		memcpy(toSend.src, ownNICaddr, sizeof(CnetNICaddr));
		memcpy(toSend.type, &code, sizeof(toSend.type));
		
		//armamos el pedido ARP
		arp_request.htype = htons(ETH_CODE);
		arp_request.ptype = htons(IP_CODE);
		arp_request.hlen = LEN_NICADDR;
		arp_request.plen = IP_ADDRLEN;
		arp_request.opcode = ARP_REQUEST_CODE;
		memcpy(arp_request.sha, ownNICaddr, sizeof(CnetNICaddr));
		arp_request.spa = network_local_address();
		//omitimos arp_request.tha porque es justamente lo que no sabemos		
		arp_request.tpa = destIP;
	
		memcpy(toSend.payload, &arp_request, sizeof(arp_request));
	}
	
	len = sizeof(toSend);
	CHECK(CNET_write_physical(link, &toSend, &len));
}

/* Imprime la tabla ARP actual del host. Para propósitos de debugging */
static EVENT_HANDLER(imprimir_tabla)
{
	int i=0;
	char nicaddr[18];
	for (i=0; i<arp_table.tam; i++)
	{
		printf("%s\t\t", network_display_address(arp_table.ipaddresses[i]));
		CNET_format_nicaddr(nicaddr, arp_table.nicaddresses[i]);
		printf("%s\n", nicaddr);
	}
}

/* símil función main() en CNET. Se especifica frente a qué eventos reaccionar y
   el nombre de los manejadores a los cuales llamar frente a estos eventos. El
   tercer parámetro no se usa (siempre 0). Se inicializa la tabla ARP del host.
*/
EVENT_HANDLER(reboot_node){
	arp_table.tam = 0;
	CHECK(CNET_set_handler(EV_PHYSICALREADY, recibir_trama,0));
	CHECK(CNET_set_handler(EV_NETWORKREADY, enviar_trama, 0));
	CHECK(CNET_set_handler(EV_DEBUG0, imprimir_tabla, 0));
	CHECK(CNET_set_debug_string(EV_DEBUG0, "ARP TABLE"));
	CNET_enable_application(ALLNODES);
}

/*----------------------FUNCIONES AUXILIARES----------------------------------*/

/* Transforma una secuencia de caracteres "pelada" en un paquete ARP recibida en
   la capa física con la estructura correspondiente. Hace la conversión necesa-
   ria entre la arquitectura de la red y la del host
   PRE: {string != NULL y arp_pack != NULL)}
*/
void raw_string_to_arp_packet(char *string, ARP_PACKET *arp_pack)
{
	char *aux = string; //puntero para recorrer el string
	assert (string != NULL && arp_pack != NULL);
	
	memcpy(&arp_pack->htype, aux, sizeof(uint16_t));
	arp_pack->htype = ntohs(arp_pack->htype);
	aux += sizeof(uint16_t);
	
	memcpy(&arp_pack->ptype, aux, sizeof(uint16_t));
	arp_pack->ptype = ntohs(arp_pack->ptype);
	aux += sizeof(uint16_t);
	
	memcpy(&arp_pack->hlen, aux, sizeof(uint8_t));
	aux += sizeof(uint8_t);
	
	memcpy(&arp_pack->plen, aux, sizeof(uint8_t));
	aux += sizeof(uint8_t);
	
	memcpy(&arp_pack->opcode, aux, sizeof(uint16_t));
	aux += sizeof(uint16_t);
	
	memcpy(arp_pack->sha, aux, sizeof(CnetNICaddr));
	aux += sizeof(CnetNICaddr);
	
	memcpy(&arp_pack->spa, aux, sizeof(IPAddress));
	aux += sizeof(IPAddress);
	
	if (arp_pack->opcode == ARP_REPLY_CODE) //si es un pedido tha indefinido
		memcpy(arp_pack->tha, aux, sizeof(CnetNICaddr));
	
	aux += sizeof(CnetNICaddr);
	
	memcpy(&arp_pack->tpa, aux, sizeof(IPAddress));
}
		
/* Se encarga de manejar la recepción de paquetes ARP. Implementa el algoritmo
   definido en RFC826 (http://www.faqs.org/rfcs/rfc826.html).
   Si todo va bien retorna 0, si hubo algún error devuelve -1 e imprime una bre-
   ve descripción del mismo. Como se la llama después de raw_string_to_arp_pack
   los campos del paquete ya han sido convertidos de la arquitectura de la red a
   la arquitectura del host.
*/		
int handle_incoming_arp_packet(ARP_PACKET *arp_pack)
{
	bool Merge_Flag = false;
	char senderha[18];
	const char *senderpa = network_display_address(arp_pack->spa);

		
	CNET_format_nicaddr(senderha, arp_pack->sha);
	printf("Recibido paquete ARP desde %s\n", senderha);		
	if (arp_pack->htype == ETH_CODE)
	{
		if (arp_pack->hlen == LEN_NICADDR)
		{
			if (arp_pack->ptype == IP_CODE)
			{
				if (arp_pack->plen == IP_ADDRLEN)
				{
					int pos = 0;
					pos = arp_table_search(arp_pack->spa);
					if (pos != -1)
					{
						printf("Actualizando tabla para %s\n", senderpa);
						update_arp_table(arp_pack->sha, pos);
						Merge_Flag = true;
					}
					if (arp_pack->tpa == network_local_address())
					{
						if (!Merge_Flag)
						{
							printf("Agregando %s a la tabla\n", senderpa);
							IPAddress spa = arp_pack->spa;
							add_arp_table(spa, arp_pack->sha);
						}
						if (arp_pack->opcode == ARP_REQUEST_CODE)
						{
							CnetNICaddr aux1;
							IPAddress auxIP=0, auxIP2=0;
							
							printf("Respondiendo pedido ARP a %s\n", senderpa);
							arp_pack->opcode = ARP_REPLY_CODE;
							
							//swapeamos los campos de dirección NIC
							memcpy(aux1, arp_pack->sha, sizeof(CnetNICaddr));
							memcpy(arp_pack->sha, linkinfo[1].nicaddr, sizeof(CnetNICaddr));
							memcpy(arp_pack->tha, aux1, sizeof(CnetNICaddr));
							 
							//swapeamos los campos de dirección IP
							auxIP = arp_pack->spa;
							auxIP2 = network_local_address();
							arp_pack->spa = auxIP2;
							arp_pack->tpa = auxIP;

							send_arp_reply(arp_pack);
						}
					}
				}
				else
				{
					printf("Longitud de protocolo IP != 4!\n");
					return -1;
				}
			}
			else
			{
				printf("Protocolo distinto de IP!\n");
				return -1;
			}
		}
		else
		{
			printf("Longitud de dirección NIC=%d != 6!\n", arp_pack->hlen);
			return -1;
		}
	}
	else
	{
		printf("Protocolo de la capa física no es Ethernet!\n");
		return -1;
	}
	return 0;
}

/* Busca la dirección NIC asociada a la dirección IP ip en la tabla ARP.
   Si está devuelve el índice dentro de la tabla; sino -1.
   PRE = {network_is_valid(ip)}
   POST = {((0 <= pos < arp_table.tam) && (arp_table.ipaddresses[pos] == ip)) OR
		   (pos==-1 AND ipadresses[i] != i para todo i: 0 <= i<arp_table.tam)}	
*/
int arp_table_search(const IPAddress ip)
{
	int i=0, pos=-1;
	IPAddress currentIP=0;

	assert(network_is_valid(ip));
	for (i=0; i<arp_table.tam; i++)
	{
		currentIP = arp_table.ipaddresses[i];
		if (currentIP == ip)
		{
			pos = i;
			break;
		}
	}
	return pos;
}
/* Actualiza el valor asociado a la dirección IP en la posición pos dentro de la
   tabla ARP.
   PRE = {0 <= pos < arp_table.tam}
   POST = {arp_table.nicaddresses[pos] == na}
*/
void update_arp_table(CnetNICaddr na, int pos)
{
	assert (0 <= pos && pos < arp_table.tam);
	memcpy(arp_table.nicaddresses[pos], na, sizeof(CnetNICaddr));
}
 
/* Envía la respuesta armada en handle_incoming_arp en caso de que haya sido un
   pedido. Notar que handle_incoming_arp no hace los cambios correspondientes
   entre arquitecturas, sino que se hacen aquí.
   PRE = {arp_pack != NULL}
*/
void send_arp_reply(ARP_PACKET *arp_pack)
{
	assert(arp_pack != NULL);
	int link = LAN_LINK;
	ETH_FRAME frame;
	uint16_t code = 0;
	size_t len = 0;
	
	code = ARP_CODE;
	memcpy(frame.dest, arp_pack->tha, sizeof(CnetNICaddr));
	memcpy(frame.src, arp_pack->sha, sizeof(CnetNICaddr));
	memcpy(frame.type, &code, sizeof(frame.type));
	memcpy(frame.payload, arp_pack, sizeof(ARP_PACKET));
	
	len = sizeof(frame);
	CHECK(CNET_write_physical(link, &frame, &len));
}

/* Agrega una nueva entrada en la tabla ARP asociando ip con na */
void add_arp_table(IPAddress ip, CnetNICaddr na)
{
	assert (network_is_valid(ip));
	arp_table.ipaddresses[arp_table.tam] = ip;
	memcpy(na, arp_table.nicaddresses[arp_table.tam],sizeof(CnetNICaddr));
	arp_table.tam += 1;
}